package project3;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

import project1.FetchResponse;
import project1.IResponse;
import project1.MainClass;
import project1.ServiceRegister;
import project3.Util.CellType;

public class SourceRouter {
	private static Random ran;
	private static Map<Long, String> routerList;
	private static Map<String, Long> routerIdList;
	private static Map<Integer, BrowserFacingThread> bftMap;
	private static final int MAX_CIRC_ID = 65536;
	
	// key << 16 is the connected router id.
	private static Map<Integer, String> circuitToRouterList;
	private static Map<String, Integer> routerToCircuitList;
	private static Integer streamID;
	private ServerSocket socket;
	private static ServiceRegister register;
	private static String routerPrefix;
	
	public SourceRouter(int routerPort, int serverPort, String ip, int p, String routerid) throws Exception {
		try {
			if (routerList == null) {
				SourceRouter.ran = new Random();
				routerList = new ConcurrentHashMap<Long, String>();
				circuitToRouterList = new ConcurrentHashMap<Integer, String>();
				routerToCircuitList = new ConcurrentHashMap<String, Integer>();
				routerIdList = new ConcurrentHashMap<String, Long>();
				bftMap = new ConcurrentHashMap<Integer, BrowserFacingThread>();
				SourceRouter.streamID = 0;
				this.socket = new ServerSocket(serverPort);
				String[] routerIdArray = routerid.split("-");
				SourceRouter.routerPrefix = routerIdArray[0] + "-" + routerIdArray[1];
				
				// Get the register for the registration agency.
				MainClass.main(new String[]{ip, p + ""});
				register = MainClass.getRegister();
				
				// Register this current Node.
				IResponse res = register.sendRegister(routerid, "" + routerPort, "" + routerIdArray[2]);
				if (res == null || res.getType() != 0x02) {
					throw new IllegalStateException("Registration failure.");
				}
				
				new Thread() {
					public void run() {
						while (!socket.isClosed()) {
							IResponse res = register.sendFetch(routerPrefix);
							if (res.getClass() != FetchResponse.class) {
								throw new IllegalStateException("Fail to get the fecth response");
							}
							FetchResponse fres = (FetchResponse) res;
							List<String[]> services = fres.getOtherService();
							
							synchronized (routerList) {
								routerList.clear();		
								for (String[] router : services) {
									String ip = router[0];
									String port = router[1];
									long rid = (Long.parseLong(router[2], 16) & 0xFFFF);
									routerList.put(rid, ip + ":" + port);
									routerIdList.put(ip + ":" + port, rid);
								}
							}
						}
						try {
							Thread.sleep(2000);
						} catch (InterruptedException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				}.start();
			}
		} catch (Exception e) {
			routerList = null;
			throw e;
		}
	}
	
	/**
	 * Listen the request from the browser. Create a new socket thread to handle new connection.
	 * 
	 * throw Exception if the port is not successfully bind.
	 * 
	 */
	public void listenBrowserRequest() { 
		new Thread() {
			
			@Override
			public void run() {
				while (true) {
					try {
						Socket soc = socket.accept();
						BrowserFacingThread thread = new BrowserFacingThread(soc);
						thread.start();
					} catch (IOException e) {
						//TODO: Handle the error when browser connection to the server error
					}
				}
			}
		}.run();
	}
	
	/**
	 * Fetch new active router tables and refresh the routerList. Create a new circuit. 
	 * If failure, try multiple times (to the same router then to other routers).
	 * TODO: Figure out a way to handle the case when creation always fails. (To user and to router)
	 * 
	 * @return circuit id
	 */
	private static int createCircuit(String ipPort, long routerId, int sid) {
		int circId = -1;
		int retryTimes = 0;
		circId = ran.nextInt(MAX_CIRC_ID);
		for (int i = 0; i < 3; i++) {
			if (i == 0) {
				Cell createCell = new CreateCell(circId, CellType.CREATE);
				try {
					Router.sendMassageToRouter(ipPort, createCell);
					if (Router.getWantedCell(circId).getCellType() == CellType.CREATE_FAILED) {
						if (retryTimes ++ >= 5) {
							System.out.println("[SRouter] Fail to create new Circuit");
							circId = -1;
							break;
						}
						i = 0;
						circId = ran.nextInt(MAX_CIRC_ID);
						continue;
					} else {

						System.out.println("[SRouter] First extension suceess");
					}
				} catch (Exception e) {
					// TODO: Handle possible different exception here.
					e.printStackTrace();
					if (retryTimes ++ >= 5) {
						System.out.println("[SRouter] Fail to create new Circuit");
						circId = -1;
						break;
					}
					i = 0;
					circId = ran.nextInt(MAX_CIRC_ID);
					continue;
				}
			} else {
				Object[] routerIdList = routerList.keySet().toArray();
				long nextRouterId = (Long) routerIdList[ran.nextInt(routerIdList.length)];
				String nextRouterIpPort = routerList.get(nextRouterId);
				byte[] body = nextRouterIpPort.getBytes();
				RelayCell rcell = new RelayCell(circId, sid, body.length, (byte) 6, body);
				try {
					Router.sendMassageToRouter(ipPort, rcell);
					if (((RelayCell) Router.getWantedCell(circId)).getRelayCmdType() == 0x0c) {
						if (retryTimes ++ >= 5) {
							System.out.println("[SRouter] Fail to create new Circuit in layer " + i);
							Router.sendMassageToRouter(ipPort, new DestroyCell(circId, CellType.DESTROY));
							circId = -1;
							break;
						}
						i --; 
						continue;
					} else {

						System.out.println("[SRouter] Extension " + i + " Success.");
					}
				} catch (Exception e) {
					if (retryTimes ++ >= 5) {
						System.out.println("[SRouter] Fail to create new Circuit in layer " + i);
						try {
							Router.sendMassageToRouter(ipPort, new DestroyCell(circId, CellType.DESTROY));
						} catch (Exception e1) {
							System.out.println("[SRouter] Send destroy message error.");
						}
						circId = -1;
						break;
					}
					i --; 
					continue;
				}
			}
		}

		circuitToRouterList.put(circId, ipPort);
		routerToCircuitList.put(ipPort, circId);
		System.out.println("[SRouter] Success to create the circuit: " + circuitToRouterList);
		
		return circId;
	}
	
	/**
	 * Choose a random circuit. If circuit does not exist, create a new circuit.
	 * 
	 * @return
	 */
	public static int chooseCircuit(int streamId) {
		while (routerList.size() == 0) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		synchronized (routerList) {
			// TODO: Can check whether there are routers not in new router list but in the old one.
			//		 Do some ciruit removing about the old routers
		
			Object[] routerIdList = routerList.keySet().toArray();
			Long nextRouterId = (Long) routerIdList[ran.nextInt(routerIdList.length)];
			String nextRouterIpPort = routerList.get(nextRouterId);
			int cirId = -1;
			if (!routerToCircuitList.containsKey(nextRouterIpPort)) {
				cirId = createCircuit(nextRouterIpPort, nextRouterId, streamId);
			} else {
				cirId = routerToCircuitList.get(nextRouterIpPort);
			}
			System.out.println("[SRouter] Choose Circuit " + cirId);
			return cirId;
		}
	}
	
	/**
	 * Create a new unique stream id.
	 * Synchornized.
	 */
	public static int generateNextStreamId() {
		synchronized (bftMap) {
			int retID = streamID + 1;
			streamID++;
			return retID;
		}
	}
	
	public static long getRouterId(String ipPort) {
		return routerIdList.get(ipPort);
	}
	
	public static String getRouterIpPort(long id) {
		return circuitToRouterList.get((int) id);
	}

	public static boolean isInSourceRouter(int curCellCircId) {
		return circuitToRouterList.containsKey(curCellCircId);
	}

	public static void sendBackToBrowser(byte[] byteListArr, int curStreamId,
			int curCellCircId) {
		System.out.println("[SRouter] Want " + curStreamId + " in " + bftMap);
		bftMap.get(curStreamId).writeBackToBrowser(byteListArr);
	}
	
	public static void registerBrowserFacingThread(int streamId, BrowserFacingThread bft) {
		synchronized (bftMap) {
			bftMap.put(streamId, bft);
			System.out.println("[SRouter] Register BFThread: " + bftMap);	
		}
	}

	public static void closeBrowserConnection(int curStreamId,
			int curCellCircId) {
		System.out.println("[SRouter] Finish one request. ");
		//bftMap.get(curStreamId).terminate();
		//bftMap.remove(curStreamId);
	}
	
	public static void sendSourcePackage(RelayCell c, int circId) {
		try {
			Router.sendMassageToRouter(circuitToRouterList.get(circId), c);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void sendSourcePackage(List<RelayCell> cells, int circId) {
		synchronized (circuitToRouterList) {
			for (RelayCell c : cells) {
				sendSourcePackage(c, circId);
			}
		}
	}
}
